﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Security.Cryptography.X509Certificates;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using System.Web.Security;
using NTQOnlineExam.Services.CommandService.Email;
using NTQOnlineExam.Services.DBCommand.Account;
using NTQOnlineExam.Services.EntityFW.Context;
using NTQOnlineExam.Services.EntityFW.Entity;
using NTQOnlineExam.Services.Infrastructure;
using NTQOnlineExam.Services.Infrastructure.Encryption;
using NTQOnlineExam.Services.Infrastructure.Utility;
using NTQOnlineExam.Services.Models;
using NTQOnlineExam.Services.ReportService.Account;
using WebMatrix.WebData;

namespace NTQOnlineExam.Services.CommandService.Account
{
    public class AccountCommandService : IAccountCommandService
    {
        private readonly ICommandExecutor _executor;
        private readonly IAccountReportService _accountReportService;
        private readonly IEmailService _emailService;
        private readonly IEncryptionService _encryptionService;
        public AccountCommandService(ICommandExecutor executor, IAccountReportService accountReportService, IEmailService emailService, IEncryptionService encryptionService)
        {
            _executor = executor;
            _accountReportService = accountReportService;
            _emailService = emailService;
            _encryptionService = encryptionService;
        }

        public void CreateUserWithAccount(CreateUserAccountCommand command)
        {
            Guard.ArgumentIsNotNull(command, "command");
            Guard.Against<ArgumentException>(string.IsNullOrWhiteSpace(command.Username), "Username must be not null");
            Guard.Against<ArgumentException>(!EmailUtilities.ValidateEmailAddress(command.Email), "The email is invalid format.");
            Guard.Against<ArgumentException>(!string.IsNullOrWhiteSpace(command.PhoneNumber) && !PhoneNumberUtilities.ValidatePhoneNumber(command.PhoneNumber), "The phone number is invalid format.");
            var existsEmail = _accountReportService.GetByEmail(command.Email);
            Guard.Against<ArgumentException>(existsEmail.Count > 0, "This email is used by another account.");
            WebSecurity.CreateUserAndAccount(command.Username, command.Password);
            var roleProvider = (SimpleRoleProvider)Roles.Provider;
            if (command.Roles != null && command.Roles.Count > 0)
            {
                roleProvider.AddUsersToRoles(new[] { command.Username }, command.Roles.ToArray());
            }
            _executor.Execute(new UpdateUserInfoByUsernameDbCommand(command.Username, command.FirstName,
                command.LastName, command.Email, command.PhoneNumber,
                command.BillingAddress, command.BillingPOBox, command.BillingZipCode, command.BillingCity,
                command.BillingCountry));
        }

        public void UpdateUserAccount(UpdateUserAccountCommand command)
        {
            Guard.ArgumentIsNotNull(command, "command");
            Guard.Against<ArgumentException>(command.UserId <= 0, "UserId is required");
            using (var context = new ExamOnlineDbContext())
            {
                UserProfile profile = context.UserProfiles.Find(command.UserId);
                Guard.Against<ApplicationException>(profile == null, string.Format("Can not find any user having Id = '{0}'", command.UserId));
                if (string.IsNullOrWhiteSpace(profile.UserName) && !string.IsNullOrWhiteSpace(command.Username))
                {
                    Guard.Against<ArgumentException>(string.IsNullOrWhiteSpace(command.Password), "Password is required");

                    profile.UserName = command.Username;
                    context.Entry(profile).State = System.Data.EntityState.Modified;
                    context.SaveChanges();

                    WebSecurity.CreateAccount(command.Username, command.Password);
                }
                else if (!string.IsNullOrWhiteSpace(profile.UserName) && !string.IsNullOrWhiteSpace(command.Password))
                {
                    var token = WebSecurity.GeneratePasswordResetToken(profile.UserName);
                    var result = WebSecurity.ResetPassword(token, command.Password);
                }

                var roleProvider = (SimpleRoleProvider)Roles.Provider;
                var userRoles = roleProvider.GetRolesForUser(profile.UserName);
                foreach (var userRole in userRoles)
                {
                    if (command.Roles == null || !command.Roles.Any() || !command.Roles.Contains(userRole)) roleProvider.RemoveUsersFromRoles(new[] { profile.UserName }, new[] { userRole });
                }
                if (command.Roles == null || !command.Roles.Any()) return;
                var addedRoles = command.Roles.Where(x => !userRoles.Contains(x));
                if (addedRoles.Any())
                {
                    roleProvider.AddUsersToRoles(new[] { profile.UserName }, addedRoles.ToArray());
                }
            };
        }

        public int CreateUser(CreateUserCommand command)
        {
            Guard.ArgumentIsNotNull(command, "command");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(command.FirstName), "First name can not be empty");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(command.LastName), "Last name can not be empty");
            Guard.Against<ArgumentException>(!EmailUtilities.ValidateEmailAddress(command.Email), "The email is invalid.");
            Guard.Against<ArgumentException>(!string.IsNullOrWhiteSpace(command.PhoneNumber) && !PhoneNumberUtilities.ValidatePhoneNumber(command.PhoneNumber), "The phone number is invalid format.");
            var existsEmail = _accountReportService.GetByEmail(command.Email);
            Guard.Against<ArgumentException>(existsEmail.Count > 0, "This email is used by another account.");
            using (var context = new ExamOnlineDbContext())
            {
                var profile = new UserProfile
                {
                    FirstName = command.FirstName,
                    LastName = command.LastName,
                    Email = command.Email,
                    Phone = command.PhoneNumber,
                    BillingAddress = command.BillingAddress,
                    BillingCity = command.BillingCity,
                    BillingCountry = command.BillingCountry,
                    BillingZipCode = command.BillingZipCode,
                    BillingPOBox = command.BillingPOBox
                };

                context.Entry(profile).State = System.Data.EntityState.Added;
                context.SaveChanges();
                return profile.UserId;
            }

        }

        public void UpdateUser(UpdateUserCommand command)
        {
            Guard.ArgumentIsNotNull(command, "command");
            Guard.Against<ArgumentException>(command.UserId <= 0, "UserId is required");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(command.FirstName), "First name can not be empty");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(command.LastName), "Last name can not be empty");
            Guard.Against<ArgumentException>(!EmailUtilities.ValidateEmailAddress(command.Email), "The email is invalid.");
            Guard.Against<ArgumentException>(!string.IsNullOrWhiteSpace(command.PhoneNumber) && !PhoneNumberUtilities.ValidatePhoneNumber(command.PhoneNumber), "The phone number is invalid format.");

            var existsEmail = _accountReportService.GetByEmail(command.Email);
            Guard.Against<ArgumentException>(existsEmail.Count > 1, "This email is used by another account.");
            Guard.Against<ArgumentException>(existsEmail.Count == 1 && existsEmail[0].UserId != command.UserId, "This email is used by another account.");
            using (var context = new ExamOnlineDbContext())
            {
                UserProfile profile = context.UserProfiles.Find(command.UserId);
                Guard.Against<ApplicationException>(profile == null, string.Format("Can not find any user having Id = '{0}'", command.UserId));
                profile.FirstName = command.FirstName;
                profile.LastName = command.LastName;
                profile.Email = command.Email;
                profile.Phone = command.PhoneNumber;

                profile.BillingAddress = command.BillingAddress;
                profile.BillingCity = command.BillingCity;
                profile.BillingCountry = command.BillingCountry;
                profile.BillingZipCode = command.BillingZipCode;
                profile.BillingPOBox = command.BillingPOBox;

                context.Entry(profile).State = System.Data.EntityState.Modified;
                context.SaveChanges();
            }
        }

        public void UpdateUserAddressInformation(UpdateUserAddressCommand command)
        {
            Guard.ArgumentIsNotNull(command, "command");
            Guard.Against<ArgumentException>(command.UserId <= 0, "UserId is required");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(command.FirstName), "First name can not be empty");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(command.LastName), "Last name can not be empty");
            using (var context = new ExamOnlineDbContext())
            {
                UserProfile profile = context.UserProfiles.Find(command.UserId);
                Guard.Against<ApplicationException>(profile == null, string.Format("Can not find any user having Id = '{0}'", command.UserId));
                profile.FirstName = command.FirstName;
                profile.LastName = command.LastName;

                profile.BillingAddress = command.BillingAddress;
                profile.BillingCity = command.BillingCity;
                profile.BillingCountry = command.BillingCountry;
                profile.BillingZipCode = command.BillingZipCode;
                profile.BillingPOBox = command.BillingPOBox;

                context.Entry(profile).State = System.Data.EntityState.Modified;
                context.SaveChanges();
            }
        }

        public void DeleteUser(int userId)
        {
            _executor.Execute(new DeleteUserAccountDbCommand(userId));

        }

        public void ResetPassword(ResetPasswordCommand command)
        {
            using (var context = new ExamOnlineDbContext())
            {
                UserProfile profile = context.UserProfiles.FirstOrDefault(x => x.Email == command.Email && !x.IsDeleted);
                if (profile == null) throw new InvalidDataException("Can't find any user for this email.");
                Guard.ArgumentIsNotNull(command, "command");
                Guard.Against<ArgumentException>(string.IsNullOrWhiteSpace(command.RandomString), "returnUrl");
                if (!EmailUtilities.ValidateEmailAddress(command.Email))
                {
                    throw new ArgumentException("Invalid email.");
                }
                var newPassword = Membership.GeneratePassword(8, 2);
                var dataForGeneratingToken = BuildDataForGeneratingResetPasswordToken(profile.UserId, newPassword, command.RandomString);
                var expirableToken = _encryptionService.GenerateExpirableChecksum(dataForGeneratingToken);
                var host = ConfigurationManager.AppSettings["HostingEndPoint"];
                var returnUrl = BuildReturnUrlForResetPassword(host, expirableToken);
                var email = BuildResetPasswordEmail(command.Email, profile.FirstName + " " + profile.LastName, newPassword, returnUrl);
                _emailService.SendEmail(email);

                profile.TemporaryNewPasswordToken = dataForGeneratingToken;
                context.Entry(profile).State = System.Data.EntityState.Modified;
                context.SaveChanges();

            }

        }

        public void ApplyResetPasswordToUser(ApplyResetPasswordCommand command)
        {
            using (var context = new ExamOnlineDbContext())
            {
                Guard.ArgumentIsNotNull(command, "command");
                Guard.ArgumentIsNotNull(command.Token, "token");

                var decryptedToken = _encryptionService.DecryptString(command.Token);
                var userId = GetUserIdFromToken(decryptedToken);
                var profile = context.UserProfiles.Find(userId);
                if (profile == null) throw new InvalidDataException("Can't find any user for this request.");

                //validate token
                if (!_encryptionService.VerifyExpirableChecksum(profile.TemporaryNewPasswordToken, command.Token, 24 * 60))
                {
                    throw new InvalidDataException("This token is expired. Please send new request to reset password.");
                }
                string password = ParseTokenForNewPassword(profile.TemporaryNewPasswordToken);
                var token = WebSecurity.GeneratePasswordResetToken(profile.UserName);
                WebSecurity.ResetPassword(token, password);

                profile.TemporaryNewPasswordToken = string.Empty;
                context.Entry(profile).State = System.Data.EntityState.Modified;
                context.SaveChanges();
                WebSecurity.Login(profile.UserName, password);
            }
        }

        private string ParseTokenForNewPassword(string token)
        {
            var temp = token.Split('_');
            if (temp.Length != 3) throw new InvalidDataException("Token using for changing password is invalid.");
            return temp[1];
        }


        private string BuildDataForGeneratingResetPasswordToken(int userId, string newPassword, string returnUrl)
        {

            var data = string.Format("{0}_{1}_{2}", userId, newPassword, returnUrl);
            return data;
        }

        private string BuildReturnUrlForResetPassword(string hostName, string token)
        {
            return string.Format("{0}{1}?token={2}", hostName, "Account/ResetPassword", token);
        }

        private MailMessage BuildResetPasswordEmail(string recipient, string name, string newPassword, string returnUrl)
        {
            var mailBody = TranslateMergeFieldForResetPassword(Resources.Account.ResetPasswordBody, returnUrl, name, newPassword);
            var mailSubject = TranslateMergeFieldForResetPassword(Resources.Account.ResetPasswordHeader, returnUrl, name, newPassword);

            var email = new MailMessage();
            email.To.Add(new MailAddress(recipient));
            email.Subject = mailSubject;
            email.Body = mailBody;
            email.IsBodyHtml = true;
            return email;
        }

        private string TranslateMergeFieldForResetPassword(string body, string url, string name, string newPassword)
        {
            var result = body.Replace("{ResetPassword_Url}", url).Replace("{ResetPassword_Name}", name).Replace("{ResetPassword_NewPassword}", newPassword);
            return result;
        }

        private int GetUserIdFromToken(string data)
        {
            var temp = data.Split('_');
            int userId;
            if (!int.TryParse(temp[0], out userId)) throw new ArgumentException("Can't find user for this request.");
            return userId;
        }
    }
}
